package DAO;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;

import common.Enums.ComponentType;
import common.Enums.DAOType;
import common.Enums.MethodType;
import common.Exceptions.DAOException;
import common.Exceptions.FormulaException;
import common.Exceptions.FrameworkError;
import common.Utils.LinkedSet;

import model.*;

/**
 * This class represents the Session instance and all of its related data. <br>
 * The class is responsible for the data manipulation and to set up a cache system for all the BC data, in order to try to reduce the database access. <br>
 * All the database access are made through this class. It first checks the cache in order to see if the required data is already loaded, 
 * if it's then immediately returns it. Otherwise, it will query the database.
 * @author D058585
 *
 */
public class SessionDAO
{
	/**
	 * Caches all the BC related data so that less databases access are required. 
	 */
	private HashMap<UUID, LinkedSet<Case>> methodCases;
	private LinkedSet<MethodApplication> methodsApplication;
	private static HashMap<UUID, BusinessCase> loadedBCs = new HashMap<UUID, BusinessCase>();
	private HashMap<UUID, Value> values;
	private HashMap<UUID, Method> methods;
	private HashMap<UUID, Criterion> criteria;
	private HashMap<UUID, Case> cases;
	private HashMap<UUID, LinkedSet<Method>> criterionRelatedMethods;
	private HashMap<UUID, LinkedSet<Case>> criterionRelatedCases;
	private HashMap<UUID, LinkedSet<Case>> bcRelatedCases;
	
	private BusinessCase cachedBC;
	private static SessionDAO instance;
	
	public SessionDAO()
	{
		cachedBC = null;
		methodCases = new HashMap<UUID, LinkedSet<Case>>();
		methodsApplication = new LinkedSet<MethodApplication>();
		cases = new HashMap<UUID, Case>();
		criteria = new HashMap<UUID, Criterion>();
		methods = new HashMap<UUID, Method>();
		values = new HashMap<UUID, Value>();
		criterionRelatedMethods = new HashMap<UUID, LinkedSet<Method>>();
		criterionRelatedCases = new HashMap<UUID, LinkedSet<Case>>();
		bcRelatedCases = new HashMap<UUID, LinkedSet<Case>>();
	}
	
	public static SessionDAO getInstance()
	{
		if(instance == null)
			instance = new SessionDAO();
		
		return instance;
	}
	
	/*public void addLoadedBC(BusinessCase bc)
	{
		if(bc != null)
		{
			if(!loadedBCs.containsKey(bc.getID()))
				loadedBCs.put(bc.getID(), bc);
		}
	}*/
	
	/*public void addAllLoadedBC(List<BusinessCase> bcs)
	{
		for(BusinessCase bc : bcs)
		{
			if(!loadedBCs.containsKey(bc.getID()))
				loadedBCs.put(bc.getID(), bc);
		}
	}*/
	
	public void addCaseToCache(Case caso)
	{
		if(!cases.containsKey(caso))
			cases.put(caso.getID(), caso);
	}
	
	public void addMethodToCache(Method met)
	{
		if(!methods.containsKey(met))
			methods.put(met.getID(), met);
	}
	
	public void addCriterionToCache(Criterion crt)
	{
		if(!criteria.containsKey(crt))
			criteria.put(crt.getID(), crt);
	}
	
	public void addValueToCache(Value val)
	{
		if(!values.containsKey(val))
			values.put(val.getID(), val);
	}
	
	public Collection<Case> getAllCachedCases()
	{
		return cases.values();
	}
	
	public Collection<Criterion> getAllCachedCriterion()
	{
		return criteria.values();
	}
	
	public Collection<Method> getAllCachedMethods()
	{
		return methods.values();
	}
	
	/**
	 * Method returns the value that is associated with a given case and a given method. <br>
	 * It first search in the cached Method applications instances, if the data is not found then it search the database.
	 * @param casID Related Case
	 * @param metID Related Method
	 * @return The value related to the Case and Method
	 * @throws DAOException If some data access related error occur. 
	 */
	public Value getValue(UUID casID, UUID metID) throws DAOException
	{
		Value retVal = null;
		
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getCaso() != null && metApp.getCaso().getID().equals(casID) && metApp.getMethod() != null && metApp.getMethod().getID().equals(metID))
			{
				retVal = metApp.getValue();
				break;
			}
		
		if(retVal == null)
			 retVal = DAOFactory.getDAOFactory(DAOType.MYSQL).getMetAppDAO().getValue(casID, metID);
		
		return retVal;
	}
	
	public BusinessCase getCachedBC() 
	{
		return cachedBC;
	}

	public void setCachedBC(BusinessCase cachedBC) {
		this.cachedBC = cachedBC;
	}
	
	public List<BusinessCase> getLoadedBCs() throws DAOException 
	{
		List<BusinessCase> bcList = new LinkedList<BusinessCase>();
		
		List<BusinessCase> bcs = DAOFactory.getDAOFactory(DAOType.MYSQL).getBcDAO().getAll();
		for(BusinessCase bc : bcs)
			if(!loadedBCs.containsKey(bc.getID()))
				loadedBCs.put(bc.getID(), bc);
		
		bcList.addAll(loadedBCs.values());
		return bcList;
	}
	
	public void clearCache()
	{
		instance = null;
		System.out.println("The cache from the session has been cleaned: " + instance);
	}
	
	public void clearMethodApplicationCache()
	{
		methodCases = new HashMap<UUID, LinkedSet<Case>>();
		methodsApplication = new LinkedSet<MethodApplication>();
		cases = new HashMap<UUID, Case>();
		criteria = new HashMap<UUID, Criterion>();
		methods = new HashMap<UUID, Method>();
		values = new HashMap<UUID, Value>();
		criterionRelatedMethods = new HashMap<UUID, LinkedSet<Method>>();
		criterionRelatedCases = new HashMap<UUID, LinkedSet<Case>>();
	}
	
	public LinkedSet<Case> getCriterionRelatedCases(UUID crtID)
	{
		LinkedSet<Case> relatedCases = new LinkedSet<Case>();
		
		try
		{
			if(criterionRelatedCases.containsKey(crtID))
				relatedCases = criterionRelatedCases.get(crtID);
			else
			{
				for(MethodApplication metApp : methodsApplication)
					if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(crtID))
						relatedCases.add(metApp.getCaso());
				
				criterionRelatedCases.put(crtID, relatedCases);
			}
		}
		catch(Exception ex)
		{
			System.out.println(ex.getMessage());
		}
		
		return relatedCases;
	}
	
	public LinkedSet<Method> getCriterionRelatedMethods(UUID crtID)
	{
		LinkedSet<Method> relatedMethods = new LinkedSet<Method>();
		
		if(criterionRelatedMethods.containsKey(crtID))
			relatedMethods = criterionRelatedMethods.get(crtID);
		else
		{
			for(MethodApplication metApp : methodsApplication)
				if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(crtID))
					relatedMethods.add(metApp.getMethod());
			
			criterionRelatedMethods.put(crtID, relatedMethods);
		}
		
		return relatedMethods;
	}
	
	public LinkedSet<Case> getBcRelatedCases(UUID bcID)
	{
		LinkedSet<Case> relatedCases = new LinkedSet<Case>();
		
		try
		{
			if(bcRelatedCases.containsKey(bcID))
				relatedCases = bcRelatedCases.get(bcID);
			else
			{
				for(MethodApplication metApp : methodsApplication)
					if(metApp.getBc().getID().equals(bcID))
						relatedCases.add(metApp.getCaso());
				
				bcRelatedCases.put(bcID, relatedCases);
			}
		}
		catch(Exception ex)
		{
			System.out.println(ex.getMessage());
		}
	
		return relatedCases;
	}
	
	private void populateCalMetParameters(CalculationMethod calMet)
	{
		//Problem: If the BC has two occurrences of the same calculation method but in different criteria, the method will add all the criteria as parameter
		//but in reality it should add only the criteria that is related to the top level criterion. 
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getParentMetID() != null && metApp.getParentMetID().equals(calMet.getID()))
				calMet.addParamater(getCriterionByID(metApp.getCriterion().getID()));
	}
	
	public LinkedSet<CalculationMethod> getCriterionCalculationMethods(UUID crtID)
	{
		LinkedSet<CalculationMethod> relatedMethods = new LinkedSet<CalculationMethod>();
		
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(crtID) && metApp.getMethod() != null && metApp.getMethod().getType() == MethodType.CALCULATION)
			{
				CalculationMethod calMet = (CalculationMethod)metApp.getMethod();
				//TODO: Think in a better way to populate the method parameter, maybe create a new table in the database...
				//Or maybe create a default formula to encapsulate the parameters...
				if(calMet.getFormula() == null)
					populateCalMetParameters(calMet);
				
				relatedMethods.add(calMet);
			}
		
		return relatedMethods;
	}
	
	public CalculationMethod getCriterionCalculationMethod(UUID crtID, UUID casID)
	{
		CalculationMethod relatedMethod = null;
		
			for(MethodApplication metApp : methodsApplication)
				if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(crtID) && metApp.getMethod().getType() == MethodType.CALCULATION
				&& metApp.getCaso().getID().equals(casID))
					relatedMethod = (CalculationMethod)metApp.getMethod();
		
		return relatedMethod;
	}
	
	public CalculationMethod getCalculationMethodByValueID(UUID valID)
	{
		CalculationMethod calMet = null;
		
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getValue() != null && metApp.getValue().getID().equals(valID)) 
			//&& metApp.getParentMetID() != null) -> Don't know why this code was here, make sure that is really not necessary
			{
				Method met = metApp.getMethod();
				if(met.getType() == MethodType.CALCULATION)
				{
					calMet = (CalculationMethod)met;
					break;
				}
			}
		
		return calMet;
	}
	
	public CalculationMethod getDerivedMeasureCalculationMethod(UUID derMesID, UUID parentID)
	{
		CalculationMethod calMethod = null;
		
			for(MethodApplication metApp : methodsApplication)
				if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(derMesID) && metApp.getParentMetID().equals(parentID) 
						&& metApp.getMethod().getType() == MethodType.CALCULATION)
				{
					calMethod = (CalculationMethod)metApp.getMethod();
					break;
				}
			
		
		return calMethod;
	}
	
	public LinkedSet<Criterion> getTopLevelCriteria()
	{
		LinkedSet<Criterion> criteria = new LinkedSet<Criterion>();
		
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getParentMetID() == null && metApp.getCriterion() != null)
			{
				Criterion crt = metApp.getCriterion();
				criteria.add(crt);
			}
		
		return criteria;
	}	
	
	public Value getRelatedValue(UUID crtID, UUID casID) throws FormulaException, DAOException
	{
		return MethodApplication.getRelatedValue(crtID, casID, methodsApplication);
	}
	
	public LinkedSet<Case> getMethodRelatedCases(UUID metID)
	{
		LinkedSet<Case> relatedMethods = new LinkedSet<Case>();
		if(methodCases.containsKey(metID))
			relatedMethods = methodCases.get(metID);
		else
		{
			for(MethodApplication metApp : methodsApplication)
				if(metApp.getMethod() != null && metApp.getMethod().getID().equals(metID))
					relatedMethods.add(metApp.getCaso());
			
			methodCases.put(metID, relatedMethods);
		}
		
		return relatedMethods;
	}
	
	private void addCaseToMethod(UUID metID, Case caso)
	{
		if(metID != null)
		{
			if(methodCases.containsKey(metID))
				methodCases.get(metID).add(caso);
			else
			{
				LinkedSet<Case> cases = new LinkedSet<Case>();
				cases.add(caso);
				
				methodCases.put(metID, cases);
			}
		}
	}
	
	/**
	 * Method add the related MethodApplication instance to the cache. Along with the MethodApplication, it add each of the component to a mapping
	 * based on the Component ID and the Component instance pair.  
	 * @param metApp The MethodApplication instance to be added.
	 */
	public void addMethodApplication(MethodApplication metApp)
	{
		if(methodsApplication == null)
			methodsApplication = new LinkedSet<MethodApplication>();
		
		methodsApplication.add(metApp);
		
		Case caso = metApp.getCaso();
		Method met = metApp.getMethod();
		Criterion crt = metApp.getCriterion();
		Value val = metApp.getValue();
		
		if(crt != null)
			criteria.put(crt.getID(), crt);
		if(caso != null)
			cases.put(caso.getID(), caso);
		if(val != null)
			values.put(val.getID(), val);
		if(met != null)
		{
			methods.put(met.getID(), met);
			addCaseToMethod(met.getID(), caso);
		}
	}
	
	/**
	 * Method removes the case from the current business Case. It execute as follows: <br>
	 * 1- The occurrences are searched by the method applications that references the case. If any is found, then it is removed. <br>
	 * 2- The cache is cleared: The case is removed from any component's related cached list. <br>
	 * 3- Finally, an assert is made in order to find out if the case is reused. If it's then don't remove it from database, otherwise remove it. <br>
	 * @param caseID The ID of the case to be removed.
	 * @throws Exception The exception that was the cause for the method to crash.
	 */
	public void removeCase(UUID caseID) throws Exception 
	{
		//Executing first step...
		for(Iterator<MethodApplication> iterator = methodsApplication.iterator() ; iterator.hasNext();)
		{
			MethodApplication current = iterator.next();
			if(current.getCaso().getID().equals(caseID))
			{
				DAOFactory.getDAOFactory(DAOType.MYSQL).getMetAppDAO().delete(current.getID());
				iterator.remove();
			}
		}
			
		//Executing second step...
		for(LinkedSet<Case> cases : methodCases.values())
			for(Iterator<Case> iterator = cases.iterator() ; iterator.hasNext();)
			{
				Case current = iterator.next();
				if(current.getID().equals(caseID))
					iterator.remove();
			}
		
		//Executing third step...
		if(DAOFactory.getDAOFactory(DAOType.MYSQL).getMetAppDAO().isComponentReused(ComponentType.CASE, caseID, cachedBC.getID()))
			removeComponent(ComponentType.CASE, caseID);
	}
	public void removeComponent(ComponentType cmpType, UUID cmpID) throws Exception
	{
		try 
		{
			HashMap<UUID, ?> map = null;
			Class<?> partypes[] = {UUID.class};
			java.lang.reflect.Method met = null;
			BaseDAO<?> classInstance = null;
			
			switch(cmpType)
			{
				case VALUE:
					map = values;
					classInstance = DAOFactory.getDAOFactory(DAOType.MYSQL).getValueDAO();
					break;
				case CASE:
					map = cases;
					classInstance = DAOFactory.getDAOFactory(DAOType.MYSQL).getValueDAO();
					break;
				case METHOD_APPLICATION:
					map = new HashMap<UUID, MethodApplication>();
					classInstance = DAOFactory.getDAOFactory(DAOType.MYSQL).getMetAppDAO();
					break;
				case METHOD:
					map = methods;
					classInstance = DAOFactory.getDAOFactory(DAOType.MYSQL).getMethodDAO();
					break;
				case CRITERION:
					map = criteria;
					classInstance = DAOFactory.getDAOFactory(DAOType.MYSQL).getCriterionDAO();
					break;
			}
			
			met = classInstance.getClass().getDeclaredMethod("delete", partypes);
			met.invoke(classInstance, cmpID);
			
			if(map.containsKey(cmpID))
				map.remove(map.get(cmpID));
		}
		catch (Exception e) 
		{
			throw e;
		}
	}
	
	public MethodApplication getMethodApplication(Criterion criterion, Case caso, UUID mainMetID)
	{
		MethodApplication retVal = null;
		
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(criterion.getID()) && metApp.getCaso().getID().equals(caso.getID()))
			{
				if(checkParentMetID(mainMetID, metApp.getParentMetID()))
				{
					retVal = metApp;
					break;
				}
			}
		
		return retVal;
	}
	
	public MethodApplication getMethodApplication(Criterion criterion, UUID parentMetID)
	{
		MethodApplication retVal = null;
		
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(criterion.getID()))
			{
				if(checkParentMetID(parentMetID, metApp.getParentMetID()))
				{
					retVal = metApp;
					break;
				}
			}
		
		return retVal;
	}
	
	private boolean checkParentMetID(UUID mainMetID, UUID parentMetID)
	{
		boolean retVal = false;
		
		if(mainMetID == null)
			retVal = (parentMetID == null);
		else
			retVal = (parentMetID != null && parentMetID.equals(mainMetID));
			
		return retVal;
	}
	
	/*public List<MethodApplication> getAllByCaseAndCriterionID(UUID bcID, UUID criterionID, UUID caseID) throws DAOException
	{
		List<MethodApplication> metApps = new LinkedSet<MethodApplication>();		 
		 
		 for(MethodApplication metApp : methodsApplication)
			 if(metApp.getCriterion() != null && metApp.getCriterion().getID().equals(criterionID) && metApp.getCaso().getID().equals(caseID))
				 metApps.add(metApp);
		 
		 if(metApps.size() == 0)
		 {
			 metApps = DAOFactory.getDAOFactory(DAOType.MYSQL).getMetAppDAO().getAllByCaseAndCriterionID(bcID, criterionID, caseID);
			 for(MethodApplication metApp : metApps)
				 addMethodApplication(metApp);
		 }
		 
		 return metApps;
	 }*/

	public LinkedSet<MethodApplication> getMethodsApplication() {
		return methodsApplication;
	}
	
	public BusinessCase getLoadedBC(UUID ID)
	{
		BusinessCase retVal = loadedBCs.get(ID); 
		
		if(retVal == null)
		{
			try
			{
				retVal = DAOFactory.getDAOFactory(DAOType.MYSQL).getBcDAO().getByID(ID);
				loadedBCs.put(ID, retVal);
			}
			catch(Exception ex){}
		}
		
		return retVal;
	}

	public Criterion getCriterionByID(UUID ID) 
	{
		 Criterion retVal = criteria.get(ID);
		
		if(retVal == null)
		{
			try
			{
				retVal = DAOFactory.getDAOFactory(DAOType.MYSQL).getCriterionDAO().getByID(ID);
				criteria.put(ID, retVal);
			}
			catch(Exception ex){}
		}
		
		return retVal;
	}
	
	public Case getCaseByID(UUID ID) 
	{
		Case retVal = cases.get(ID);
		
		if(retVal == null)
		{
			try
			{
				retVal = DAOFactory.getDAOFactory(DAOType.MYSQL).getCaseDAO().getByID(ID);
				cases.put(ID, retVal);
			}
			catch(Exception ex){}
			
		}
		
		return retVal;
	}
	
	public Method getMethodByID(UUID ID) 
	{
		 Method retVal = methods.get(ID);
		
		if(retVal == null)
		{
			try
			{
				retVal = DAOFactory.getDAOFactory(DAOType.MYSQL).getMethodDAO().getByID(ID);
				methods.put(ID, retVal);
			}
			catch(Exception ex){}
		}
		
		return retVal;
	}
	
	public Value getValueByID(UUID ID)
	{
		 Value retVal = values.get(ID);
		
		if(retVal == null)
		{
			try
			{
				retVal = DAOFactory.getDAOFactory(DAOType.MYSQL).getValueDAO().getByID(ID);
				values.put(ID, retVal);
			}
			catch(Exception ex){}
		}
		
		return retVal;
	}
	
	public MethodApplication getMethodApplicationByID(UUID ID) 
	{
		MethodApplication retVal = null;
		
		for(MethodApplication metApp : methodsApplication)
			if(metApp.getID().equals(ID))
			{
				retVal = metApp;
				break;
			}
		
		if(retVal == null)
		{
			try
			{
				retVal = DAOFactory.getDAOFactory(DAOType.MYSQL).getMetAppDAO().getByID(ID);
				methodsApplication.add(retVal);
			}
			catch(Exception ex){}
		}
		
		return retVal;
	}
	
	public boolean isParameterUsed(UUID metID, UUID crtID)
	{
		boolean isParameterUsed = true;
		if(metID != null && crtID != null)
		{
			Criterion parameter = getCriterionByID(crtID);
			Method met = getMethodByID(metID);
			if(met.getType() == MethodType.CALCULATION)
			{
				CalculationMethod calMet = ((CalculationMethod)met);
				isParameterUsed = calMet.getParameters().contains(parameter);
				if(!isParameterUsed && calMet.getDerivedMeasures() != null )
				{
					for(Criterion crt : calMet.getDerivedMeasures())
					{
						CalculationMethod relMethod = getDerivedMeasureCalculationMethod(crt.getID(), metID);
						if(relMethod.getParameters().contains(parameter))
						{
							isParameterUsed = true;
							break;
						}
					}
				}
			}
		}
			
		return isParameterUsed;
	}
	
	public boolean isParameter(UUID metID, UUID crtID)
	{
		boolean isParameter = true;
		if(metID != null && crtID != null)
		{
			Criterion crt = getCriterionByID(crtID);
			Method met = getMethodByID(metID);
			if(met.getType() == MethodType.CALCULATION)
				isParameter = ((CalculationMethod)met).isParameter(crt);
		}
			
		return isParameter;
	}
	
	public boolean isOrigin(MethodApplication metApp, UUID cmpID, String cmpType)
	{
		boolean retVal = false;
		MethodApplication origin = null;
		try 
		{
			if(metApp != null && cmpID != null)
			{
				ComponentType cmp = ComponentType.valueOf(cmpType.toUpperCase());
				origin = DAOFactory.getDAOFactory(DAOType.MYSQL).getMetAppDAO().getOriginMetApp(cmp, cmpID);
				if(origin.getCreatedDate().compareTo(metApp.getCreatedDate()) == 0)
					retVal = true;
				else if(origin.getCreatedDate().compareTo(metApp.getCreatedDate()) < 0 && metApp.getParentMetID().equals(origin.getParentMetID()))
					retVal = true;
			}
			else
			{
				StackTraceElement ste = Thread.currentThread().getStackTrace()[1];
				String location = "Class: " + ste.getClassName() + ", method: " + ste.getMethodName();
				showError(location, "The method application or the cmpID are null, please check the consistent of the data.", new Exception("The method application or the cmpID are null, please check the consistent of the data."));
			}
		} 
		catch (Exception e) 
		{
			e.printStackTrace(); 
		}
		
		return retVal;
	}
	
	public void showError(String errorLocation, String errorMessage, Exception e)
	{
		FrameworkError error = new FrameworkError(errorLocation, errorMessage);
		error.setStackTrace(e);
		error.setLocation(errorLocation);
		
		String debugDetails = "\n************ ERROR DETECTED - " + new Date() + "************\n\n";
		debugDetails += "- Exception detalis: " + e.getMessage() + "\n";
		debugDetails += "- Error Location -> " + errorLocation + "\n\n";
		debugDetails += error.getStackTrace();
		debugDetails += "\n\n*************************************************************************\n";
		
		error.setDebugDetails(debugDetails);
		
		System.err.println(debugDetails);
	}
}
